package gugga.tasks.containers
{
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	import gugga.collections.linkedList.LinkedList;
	import gugga.collections.linkedList.LinkedListItem;
	import gugga.collections.linkedList.LinkedListIterator;
	import gugga.debug.Assertion;
	import gugga.eventHelpers.EventDescriptor;
	import gugga.eventHelpers.Listener;
	import gugga.tasks.ITask;
	import gugga.tasks.events.TaskEvent;
	import gugga.tasks.events.TasksContainerEvent;

	[Event(name="start", type="TaskEvent")]
	[Event(name="interrupted", type="TaskEvent")]
	[Event(name="completed", type="TaskEvent")]
	[Event(name="taskAdded", type="TasksContainerEvent")]
	[Event(name="taskRemoved", type="TasksContainerEvent")]

	/**
	 * <code>TaskSequence</code> is a container for sequentially executing tasks, 
	 * and it is a task by itself. It will raise <b><i>completed</i></b> event, 
	 * only if it is started through its <code>start()</code> method, and all of 
	 * the tasks in the list are also <b><i>completed</i></b>.
	 * <p>
	 * The tasks container is arranged as a <code>LinkedList</code>. We iterate to 
	 * the next task, only when the previous task is <b><i>completed</i></b>. 
	 * <p>
	 * <code>TaskSequence</code> provides functionallity to edit the 
	 * <code>TasksList</code> when the <code>TaskSequence</code> is already 
	 * started. The only limitation is that the currently executing task can't be 
	 * deleted - an attempt to do this will raise an exception. 
	 * 
	 * @author Todor Kolev
	 * @see gugga.collections.linkedList.LinkedList
	 */
	public class TaskSequence extends EventDispatcher 
		implements ITask, ITasksContainer
	{
		private var mIsRunning : Boolean;
		private var mCurrentItemCompletionListener : Listener;
		
		private var mCurrentTask : ITask;
		public function get CurrentTask() : ITask { return mCurrentTask; }
	
		private var mTasksList : LinkedList;
		private var mMarkedTasks : Dictionary;
		 
		function TaskSequence()
		{	
			mIsRunning = false;
			mTasksList = new LinkedList();
			mMarkedTasks = new Dictionary();
		}
		
		public function isRunning() : Boolean 
		{
			return mIsRunning;
		}
		
		public function start() : void
		{
			mIsRunning = true;
			dispatchEvent(new TaskEvent(TaskEvent.START));
			
			var task : ITask = ITask(mTasksList.getHead());
			if(task)
			{
				startTask(task);
			}
			else if(mTasksList.Count == 0)
			{
				mIsRunning = false;
				dispatchEvent(new TaskEvent(TaskEvent.COMPLETED));
				//TODO: it was dispatchEventLater in previous version
			}
			else
			{
				Assertion.fail("Linked list failure - there are items, but no head", this, arguments);
			}
		}
		
		private function startTask(aTask : ITask) : void
		{	
			mCurrentTask = aTask;
			mCurrentItemCompletionListener = Listener.createSingleTimeListener(
				new EventDescriptor(mCurrentTask, TaskEvent.COMPLETED), 
				onTaskCompleted);
			
			mCurrentTask.start();
		}
			
		private function onTaskCompleted(aEvent : TaskEvent) : void
		{
			if(!mTasksList.isTail(mCurrentTask))
			{
				var nextTask : ITask = getTaskAfter(mCurrentTask);
				startTask(nextTask);
			}
			else
			{
				mIsRunning = false;
				dispatchEvent(new TaskEvent(TaskEvent.COMPLETED));
			}
		}
		
		//TODO: Revise unique tasks constraint. Revise tests.	
		public function addTask(aTask:ITask) : void
		{
			Assertion.failIfReturnsTrue(
				mTasksList, mTasksList.contains, [aTask],
				"This task already exists in the sequence", this, arguments);
					
			mTasksList.insertTail(aTask);
			dispatchEvent(new TasksContainerEvent(TasksContainerEvent.TASK_ADDED, aTask, null));
		}
		
		public function addTaskAfter(aTargetTask:ITask, aTask:ITask) : void
		{
			Assertion.failIfReturnsTrue(
				mTasksList, mTasksList.contains, [aTask],
				"This task already exists in the sequence", this, arguments);
			 
			var targetTaskListItem : LinkedListItem = mTasksList.getFirstItemContaining(aTargetTask);
			 
			mTasksList.insertAfter(targetTaskListItem, aTask);
			dispatchEvent(new TasksContainerEvent(TasksContainerEvent.TASK_ADDED, aTask, null));
		}
		
		public function addTaskBefore(aTargetTask:ITask, aTask:ITask) : void
		{
			Assertion.failIfReturnsTrue(
				mTasksList, mTasksList.contains, [aTask],
				"This task already exists in the sequence", this, arguments);
			
			var targetTaskListItem : LinkedListItem = mTasksList.getFirstItemContaining(aTargetTask);
			
			mTasksList.insertBefore(targetTaskListItem, aTask);
			dispatchEvent(new TasksContainerEvent(TasksContainerEvent.TASK_ADDED, aTask, null));
		}
		
		public function removeLastTask() : void
		{
			var task : ITask = ITask(mTasksList.getTail());
			removeTask(task);
		}
	
		public function removeFirstTask() : void
		{
			var task : ITask = ITask(mTasksList.getHead());
			removeTask(task);
		}
		
		//TODO: We must not remove task which is running. Revise tests.
		public function removeTask(aTask:ITask) : void
		{
			Assertion.failIfReturnsTrue(
				this, isCurrentlyRunning, [aTask],
				"Can not delete currently running task", this, arguments);
					
			var taskListItem : LinkedListItem = mTasksList.getFirstItemContaining(aTask);
				
			mTasksList.deleteItem(taskListItem);
			dispatchEvent(new TasksContainerEvent(TasksContainerEvent.TASK_REMOVED, aTask, null));
		}
		
		public function removeTaskAfter(aTargetTask:ITask) : void
		{
			var targetTaskListItem : LinkedListItem = mTasksList.getFirstItemContaining(aTargetTask);
			
			Assertion.failIfReturnsTrue(
				this, isCurrentlyRunning, [targetTaskListItem.Data],
				"Can not delete currently running task", this, arguments);
			
			mTasksList.deleteAfter(targetTaskListItem);
			
			var task : ITask = ITask(targetTaskListItem.Data);
			dispatchEvent(new TasksContainerEvent(TasksContainerEvent.TASK_REMOVED, task, null));
		}
		
		public function removeTaskBefore(aTargetTask:ITask) : void
		{
			var targetTaskItem : LinkedListItem = mTasksList.getFirstItemContaining(aTargetTask);
			
			var precedingListItem : LinkedListItem = mTasksList.getItemPredecessor(targetTaskItem);
			var precedingTask : ITask = ITask(precedingListItem.Data);
			
			Assertion.failIfReturnsTrue(
				this, isCurrentlyRunning, [precedingTask],
				"Can not delete currently running task", this, arguments);
			
			mTasksList.deleteItem(precedingListItem);
			dispatchEvent(new TasksContainerEvent(TasksContainerEvent.TASK_REMOVED, precedingTask, null));
		}
		
		public function getAllTasks() : Array 
		{		
			var iterator : LinkedListIterator = LinkedListIterator(mTasksList.getIterator());
			var tasks : Array = new Array();
			
			while(iterator.iterate())
			{
				tasks.push(iterator.Current);
			}
			
			return tasks;
		}
	
		public function isCurrentlyRunning(aTask:ITask) : Boolean
		{
			return (aTask == mCurrentTask);
		}
		
		public function getCurrentlyRunningTask() : ITask
		{
			return mCurrentTask;
		}	
	
		public function getTaskAfter(aTask:ITask) : ITask
		{
			var iterator : LinkedListIterator = LinkedListIterator(mTasksList.getIterator());
			var nextTask : ITask;
			
			while(iterator.iterate())
			{
				if(iterator.Current == aTask)
				{
					if(iterator.iterate())
					{
						nextTask = ITask(iterator.Current);
					}
					
					break;
				}
			}
			
			return nextTask;
		}
	
		public function getTaskBefore(aTask:ITask) : ITask
		{
			var iterator : LinkedListIterator = LinkedListIterator(mTasksList.getIterator());
			var specifiedTaskExists : Boolean = false;
			var precedingTask : ITask;
			
			while(iterator.iterate())
			{
				if(iterator.Current == aTask)
				{
					specifiedTaskExists = true;
					break;
				}
				
				precedingTask = ITask(iterator.Current);
			}
			
			if(specifiedTaskExists)
			{
				return precedingTask;
			}
			else
			{
				return undefined;
			}
		}
		
		public function isImmediatelyInterruptable() : Boolean
		{
			if(!isRunning())
			{
				return true;
			}
			else
			{
				return mCurrentTask.isImmediatelyInterruptable();
			}
		}
		
		public function interrupt() : void
		{
			if(isRunning())
			{
				Listener.createSingleTimeListener(
					new EventDescriptor(mCurrentTask, TaskEvent.INTERRUPTED),
					onCurrentTaskInterrupted);
			
				mCurrentTask.interrupt();
				mCurrentItemCompletionListener.stop();
			}
			else
			{
				dispatchEvent(new TaskEvent(TaskEvent.INTERRUPTED));
			}
			
			mIsRunning = false;
		}	
		
		private function onCurrentTaskInterrupted(aEvent : TaskEvent) : void 
		{
			dispatchEvent(new TaskEvent(TaskEvent.INTERRUPTED));
		}
	
		public function getFirstTask() : ITask
		{
			return ITask(mTasksList.getHead());
		}
	
		public function getLastTask() : ITask
		{
			return ITask(mTasksList.getTail());
		}
		
		public function markTask(aTask:ITask, aMarker:String) : void
		{
			mMarkedTasks[aMarker] = aTask;
		}
		
		public function getMarkedTask(aMarker:String) : ITask
		{
			return mMarkedTasks[aMarker];
		}
		
	}
}